/*
 *   Copyright 2021 zzh
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */

package com.aduib.boot.web.context.handler;

import cn.hutool.core.convert.Convert;
import com.aduib.boot.common.concurrent.NameThreadLocal;
import com.aduib.boot.common.exception.CommonException;
import com.aduib.boot.common.json.DefaultJsonParser;
import com.aduib.boot.common.json.TypeCompositeToJson;
import com.aduib.boot.common.util.IOUtils;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.ValidationAnnotationUtils;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * @description: RequestJsonParamHandlerMethodArgumentResolver
 * @author: zzh
 * @date: 2021/9/7 11:40
 */
public class RequestJsonParamHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

  private static final Object NO_VALUE = new Object();

  private static final NameThreadLocal<Map> JSON_NODE_NAME_THREAD_LOCAL=new NameThreadLocal<Map>(RequestJsonParamHandlerMethodArgumentResolver.class.getName()+"#1");

  private static final NameThreadLocal<String> JSON_STR_NAME_THREAD_LOCAL=new NameThreadLocal<String>(RequestJsonParamHandlerMethodArgumentResolver.class.getName()+"#2");

  private static final Set<HttpMethod> SUPPORTED_METHODS =
      EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH,HttpMethod.DELETE);

  private final DefaultJsonParser defaultJsonParser =new DefaultJsonParser();

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasMethodAnnotation(RequestJsonParam.class) || parameter.hasParameterAnnotation(RequestJsonParam.class);
  }

  @Override
  public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    Object body = NO_VALUE;
    if (Objects.isNull(parameter.getMethod())){
      return null;
    }
    //方法参数个数
    int parameterCount = parameter.getMethod().getParameterCount();
    //当前参数在参数列表的索引
    int parameterIndex = parameter.getParameterIndex();
    try {
      parameter = parameter.nestedIfOptional();
      HttpServletRequest httpServerRequest = webRequest.getNativeRequest(HttpServletRequest.class);
      Assert.state(httpServerRequest != null, "No HttpServletRequest");
      ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(httpServerRequest);
      boolean noContentType = false;
      MediaType contentType;
      try {
        contentType = inputMessage.getHeaders().getContentType();
      } catch (InvalidMediaTypeException ex) {
        throw new HttpMediaTypeNotSupportedException(ex.getMessage());
      }
      if (contentType == null) {
        noContentType = true;
      }
      boolean haveRequestJsonParam = false;
      RequestJsonParam methodAnnotation = parameter.getMethodAnnotation(RequestJsonParam.class);
      if (Objects.isNull(methodAnnotation)) {
        methodAnnotation = parameter.getParameterAnnotation(RequestJsonParam.class);
        if (Objects.nonNull(methodAnnotation)) {
          haveRequestJsonParam = true;
        }
      }
      // 参数名
      String parameterName = parameter.getParameterName();
      // 方法参数类型
      Type targetType = parameter.getNestedGenericParameterType();
      // controller类型
      Class<?> contextClass = parameter.getContainingClass();
      // 方法参数类型
      Class<?> targetClass = (targetType instanceof Class ? (Class<?>) targetType : null);
      // 泛型类型集合
      ResolvableType[] generics = null;
      if (targetClass == null) {
        // 类型参数
        ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
        targetClass = (Class<?>) resolvableType.resolve();
        generics = resolvableType.getGenerics();
      }
      HttpMethod httpMethod =
          (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
      EmptyBodyCheckingHttpInputMessage message;
      try {
        message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
        Map jsonNode = null;
        String text = null;
        // 当前参数索引等于0时
        if (parameterIndex == 0) {
          ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
          IOUtils.copy(message.getBody(), baos);
          text = new String(baos.toByteArray(), StandardCharsets.UTF_8).trim();
          if (text == null) {
            return null;
          }
          jsonNode = defaultJsonParser.read(text, new TypeCompositeToJson(Map.class, null));
          JSON_NODE_NAME_THREAD_LOCAL.getThreadLocal().set(jsonNode);
          JSON_STR_NAME_THREAD_LOCAL.getThreadLocal().set(text);
        } else {
          jsonNode = JSON_NODE_NAME_THREAD_LOCAL.getThreadLocal().get();
          text = JSON_STR_NAME_THREAD_LOCAL.getThreadLocal().get();
        }
        if (jsonNode != null) {
          boolean haveRequiredParam = false;
          Object value = jsonNode.get(parameterName);
          if (Objects.nonNull(value)) {
            if (haveRequestJsonParam) {
              haveRequiredParam = methodAnnotation.required();
              body = Convert.convert(targetClass, value);
            }
          }
          try {
            if (haveRequiredParam) {
              throw new MissingServletRequestParameterException(
                  parameterName, parameter.getParameterType().toString());
            }
          } catch (RuntimeException e) {
            throw new CommonException(
                String.format(
                    "请求参数名：%s 对应的值：%s 无法绑定到方法：%s 的参数：%s 上",
                    parameterName, text, parameter.getMethod(), parameterName));
          }
        }
      } catch (IOException ex) {
        throw new HttpMessageNotReadableException(
            "I/O error while reading input message", ex, inputMessage);
      }

      if (body == NO_VALUE) {
        if (httpMethod != null
            || !SUPPORTED_METHODS.contains(httpMethod)
            || (noContentType && !message.hasBody())) {
          return null;
        }
        throw new HttpMediaTypeNotSupportedException(
            contentType, Collections.singletonList(MediaType.APPLICATION_JSON));
      }

      if (binderFactory != null) {
        String name = Conventions.getVariableNameForParameter(parameter);
        WebDataBinder binder = binderFactory.createBinder(webRequest, body, name);
        if (body != null) {
          validateIfApplicable(binder, parameter);
          if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
          }
        }
        if (mavContainer != null) {
          mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
        }
      }
    } catch (Exception e) {
      clearThreadLocal(true, parameterCount, parameterIndex);
      throw e;
    }
    clearThreadLocal(false, parameterCount, parameterIndex);
    return adaptArgumentIfNecessary(body,parameter);
  }

  private void clearThreadLocal(boolean haveEx,int parameterCount, int parameterIndex) {
    if (haveEx||(parameterIndex ==(parameterCount -1))){
      JSON_NODE_NAME_THREAD_LOCAL.getThreadLocal().remove();
      JSON_STR_NAME_THREAD_LOCAL.getThreadLocal().remove();
    }
  }

  private void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
      Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
      if (validationHints != null) {
        binder.validate(validationHints);
        break;
      }
    }
  }

  private boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
    int i = parameter.getParameterIndex();
    Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes();
    boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
    return !hasBindingResult;
  }

  protected Object adaptArgumentIfNecessary(@Nullable Object arg, MethodParameter parameter) {
    if (parameter.getParameterType() == Optional.class) {
      if (arg == null || (arg instanceof Collection && ((Collection<?>) arg).isEmpty()) ||
          (arg instanceof Object[] && ((Object[]) arg).length == 0)) {
        return Optional.empty();
      }
      else {
        return Optional.of(arg);
      }
    }
    return arg;
  }

  private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {

    private final HttpHeaders headers;

    @Nullable
    private final InputStream body;

    public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
      this.headers = inputMessage.getHeaders();
      InputStream inputStream = inputMessage.getBody();
      if (inputStream.markSupported()) {
        inputStream.mark(1);
        this.body = (inputStream.read() != -1 ? inputStream : null);
        inputStream.reset();
      }
      else {
        PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
        int b = pushbackInputStream.read();
        if (b == -1) {
          this.body = null;
        }
        else {
          this.body = pushbackInputStream;
          pushbackInputStream.unread(b);
        }
      }
    }

    @Override
    public HttpHeaders getHeaders() {
      return this.headers;
    }

    @Override
    public InputStream getBody() {
      return (this.body != null ? this.body : StreamUtils.emptyInput());
    }

    public boolean hasBody() {
      return (this.body != null);
    }
  }
}
